
#include <boost/algorithm/string.hpp>

#include "../debug.hpp"
#include "../gl.hpp"
#include "../math.hpp"

#include "Pixmap.hpp"

namespace zen {

	namespace video {
		
		Pixmap::Pixmap() {
			data = NULL;
			width = 0;
			height = 0;
			depth = 0;
			format = GL_RGB;
		}

		Pixmap::Pixmap(const Pixmap &other) {
			data = NULL;
			*this = other;
		}

		Pixmap::Pixmap(GLuint width, GLuint height, GLuint depth, GLenum format) {
			data = NULL;
			realloc(width, height, depth, format);
		}

		Pixmap::Pixmap(const std::string &fileName, GLenum allocFormat) {
			data = NULL;
			load(fileName, allocFormat);
		}

		Pixmap::~Pixmap() {
			delete[] data;
		}

		Pixmap& Pixmap::operator = (const Pixmap &other) {
			delete[] data;
			if (other.data == NULL) {
				width = 0;
				height = 0;
				depth = 0;
				format = 0;
			} else {
				GLubyte bpp = glm::max(1u, other.formatBitCount() / 8);
				size_t bc = width * height * depth * bpp;
				data = new GLubyte[bc];
				if (data != NULL) {
					width = other.width;
					height = other.height;
					depth = other.depth;
					format = other.format;
					memcpy(data, other.data, bc);
				} else {
					width = 0;
					height = 0;
					depth = 0;
					format = 0;
				}
			}
			return *this;
		}
		
		unsigned int Pixmap::formatBitCount(GLenum format) {
			if (format == GL_RGBA || format == GL_BGRA)
				return 32;
			if (format == GL_RGB || format == GL_BGR)
				return 24;
			if (format == GL_LUMINANCE_ALPHA)
				return 16;
			if (format == GL_LUMINANCE || format == GL_ALPHA || format == GL_RED || format == GL_GREEN || format == GL_BLUE)
				return 8;
			return 0;
		}

		bool Pixmap::load(const std::string &fileName, GLenum allocFormat) {
			#ifdef __ZEN_LOADER_PIX__
				if (boost::to_lower_copy(fileName.substr(fileName.length() - 4)) == ".pix")
					return loadPIX(fileName) && (allocFormat==0? true: convert(allocFormat));
			#endif
			#ifdef __ZEN_LOADER_BMP__
				if (boost::to_lower_copy(fileName.substr(fileName.length() - 4)) == ".bmp")
					return loadBMP(fileName) && (allocFormat==0? true: convert(allocFormat));
			#endif
			#ifdef __ZEN_LOADER_PNG__
				if (boost::to_lower_copy(fileName.substr(fileName.length() - 4)) == ".png")
					return loadPNG(fileName) && (allocFormat==0? true: convert(allocFormat));
			#endif
			#ifdef __ZEN_LOADER_TGA__
				if (boost::to_lower_copy(fileName.substr(fileName.length() - 4)) == ".tga")
					return loadTGA(fileName) && (allocFormat==0? true: convert(allocFormat));
			#endif
			return false;
		}

		bool Pixmap::save(const std::string &fileName, GLenum saveFormat) const {
			if (saveFormat == 0)
				saveFormat = format;

			Pixmap pixmap;
			bool success = pixmap.copy(*this) && pixmap.convert(saveFormat);

			if (success) {
				success = false;

			#ifdef __ZEN_LOADER_PIX__
				if (boost::to_lower_copy(fileName.substr(fileName.length() - 4)) == ".pix")
					success = pixmap.savePIX(fileName);
			#endif
			#ifdef __ZEN_LOADER_BMP__
				if (boost::to_lower_copy(fileName.substr(fileName.length() - 4)) == ".bmp")
					success = pixmap.saveBMP(fileName);
			#endif
			#ifdef __ZEN_LOADER_PNG__
				if (boost::to_lower_copy(fileName.substr(fileName.length() - 4)) == ".png")
					success = pixmap.savePNG(fileName);
			#endif
			#ifdef __ZEN_LOADER_TGA__
				if (boost::to_lower_copy(fileName.substr(fileName.length() - 4)) == ".tga")
					success = pixmap.saveTGA(fileName);
			#endif
			}
			return success;
		}

		bool Pixmap::realloc(GLuint width, GLuint height, GLuint depth, GLenum format) {
			unsigned int bpp = formatBitCount(format);
			if (width > 0 && height > 0 && depth > 0 && bpp > 0) {
				int bc = width * height * depth * bpp;

				delete[] data;
				data = new GLubyte[bc];

				if (data != NULL) {
					this->format = format;
					this->width = width;
					this->height = height;
					this->depth = depth;
					return true;
				}
			}

			this->data = NULL;
			this->width = this->height = this->depth = 0;
			this->format = format;
			return true;
		}

		bool Pixmap::free() {
			delete[] data;
			data = NULL;
			width = height = depth = 0;
			return true;
		}

		bool Pixmap::copy(const Pixmap &source) {
			if (source.data == NULL) {
				delete[] data;
				width = 0;
				height = 0;
				depth = 0;
				format = 0;
				return true;
			} else {
				GLubyte bpp = glm::max(1u, source.formatBitCount() / 8);
				size_t bc = width * height * depth * bpp;
				delete[] data;
				data = new GLubyte[bc];
				if (data != NULL) {
					width = source.width;
					height = source.height;
					depth = source.depth;
					format = source.format;
					memcpy(data, source.data, bc);
					return true;
				}
				width = 0;
				height = 0;
				depth = 0;
				format = 0;
			}
			return false;
		}

		bool Pixmap::convert(GLenum format, GLubyte defaultValue) {
			if (this->format == format)
				return true;

			// Inverts color components order.
			// RGBA > BGRA
			// BGRA > RGBA
			// RGB  > BGR
			// BGR  > RGB
			if ((this->format == GL_RGBA && format == GL_BGRA) || (this->format == GL_BGRA && format == GL_RGBA)
				|| (this->format == GL_RGB && format == GL_BGR) || (this->format == GL_BGR && format == GL_RGB))
			{
				GLubyte bpp = formatBitCount() / 8;
				GLuint bc = width * height * depth * bpp;
				GLubyte tmp;
				for (GLuint i = 0; i < bc; i += bpp) {
					tmp = data[i];
					data[i] = data[i + 2];
					data[i +2 ] = tmp;
				}
				this->format = format;
				return true;
			}
			// Adds alpha component and inverts color components order if needed.
			// RGB  > RGBA
			// RGB  > BGRA
			// BGR  > RGBA
			// BGR  > BGRA
			else if ((this->format == GL_RGB && (format == GL_RGBA || format == GL_BGRA))
				|| (this->format == GL_BGR && (format == GL_RGBA || format == GL_BGRA)))
			{
				GLuint pc = width * height * depth * 3;
				GLuint nc = width * height * depth * 4;
				GLubyte* tmpData = data;
				data = new GLubyte[nc];

				if ((this->format == GL_RGB && format == GL_RGBA) || (this->format == GL_BGR && format == GL_BGRA)) {
        			for (GLuint i = 0, j = 0; i < pc || j < nc; i += 3, j += 4) {
						data[j] = tmpData[i];
						data[j + 1] = tmpData[i + 1];
						data[j + 2] = tmpData[i + 2];
						data[j + 3] = defaultValue;
					}
				} else {
        			for (GLuint i = 0, j = 0; i < pc || j < nc; i += 3, j += 4) {
						data[j] = tmpData[i + 2];
						data[j + 1] = tmpData[i + 1];
						data[j + 2] = tmpData[i];
						data[j + 3] = defaultValue;
					}
				}

				delete[] tmpData;
				this->format = format;
				return true;
			}
			// Deletes alpha component and inverts color components order if needed.
			// RGBA > RGB
			// RGBA > BGR
			// BGRA > RGB
			// BGRA > BGR
			else if ((this->format == GL_RGBA && (format == GL_RGB || format == GL_BGR))
				|| (this->format == GL_BGRA && (format == GL_RGB || format == GL_BGR)))
			{
				GLuint pc = width * height * depth * 4;
				GLuint nc = width * height * depth * 3;

				GLubyte* tmpData = data;
				data = new GLubyte[nc];

				if ((this->format == GL_RGBA && format == GL_RGB) || (this->format == GL_BGRA && format == GL_BGR)) {
        			for (GLuint i = 0, j = 0; i < pc || j < nc; i += 4, j += 3) {
						data[j] = tmpData[i];
						data[j + 1] = tmpData[i + 1];
						data[j + 2] = tmpData[i + 2];
					}
				} else {
        			for (GLuint i = 0, j = 0; i < pc || j < nc; i += 4, j += 3) {
						data[j] = tmpData[i + 2];
						data[j + 1] = tmpData[i + 1];
						data[j + 2] = tmpData[i];
					}
				}

				delete[] tmpData;
				this->format = format;
				return true;
			}
			else if ((this->format==GL_LUMINANCE || this->format==GL_ALPHA || this->format==GL_RED || this->format==GL_GREEN || this->format==GL_BLUE)
				&& (format==GL_LUMINANCE || format==GL_ALPHA || format==GL_RED || format==GL_GREEN || format==GL_BLUE))
			{
    			this->format = format;
				return true;
			}
			else if ((this->format == GL_LUMINANCE || this->format == GL_ALPHA || this->format == GL_RED || this->format == GL_GREEN || this->format == GL_BLUE)
				&& format == GL_LUMINANCE_ALPHA)
			{
				GLuint pc = width * height * depth * 1;
				GLuint nc = width * height * depth * 2;

				GLubyte* tmpData = data;
				data = new GLubyte[nc];

				for (GLuint i=0, j=0; i<pc || j<nc; i+=1, j+=2) {
					data[j] = tmpData[i];
					data[j + 1] = defaultValue;
				}

				delete[] tmpData;
				this->format = format;
				return true;
			}
			else if (this->format == GL_LUMINANCE_ALPHA
				&& (format == GL_LUMINANCE || format == GL_ALPHA || format == GL_RED || format == GL_GREEN || format == GL_BLUE))
			{
				GLuint pc = width * height * depth * 2;
				GLuint nc = width * height * depth * 1;

				GLubyte* tmpData = data;
				data = new GLubyte[nc];

				for (GLuint i = 0, j = 0; i < pc || j < nc; i += 2, j += 1)
					data[j] = tmpData[i];

				delete[] tmpData;
				this->format = format;
				return true;
			}

			return false;
		}

		bool Pixmap::fill(GLubyte red, GLubyte green, GLubyte blue, GLubyte alpha) {
			if (data != NULL) {
				if (format == GL_RGBA) {
					int bc = width * height * depth * 4;
					for (int i = 0; i < bc; i += 4) {
						data[i] = red;
						data[i + 1] = green;
						data[i + 2] = blue;
						data[i + 3] = alpha;
					}
					return true;
				} else if (format == GL_BGRA) {
					int bc = width * height * depth * 4;
					for (int i = 0; i < bc; i += 4) {
						data[i] = blue;
						data[i + 1] = green;
						data[i + 2] = red;
						data[i + 3] = alpha;
					}
					return true;
				} else if (format == GL_RGB) {
					int bc = width * height * depth * 3;
					for (int i = 0; i < bc; i += 3) {
						data[i] = red;
						data[i + 1] = green;
						data[i + 2] = blue;
					}
					return true;
				} else if (format == GL_BGR) {
					int bc = width * height * depth * 3;
					for (int i = 0; i < bc; i += 3) {
						data[i] = blue;
						data[i + 1] = green;
						data[i + 2] = red;
					}
					return true;
				}
			}
			return false;
		}

		bool Pixmap::setAlphaWhereColor(GLubyte newAlpha, GLubyte red, GLubyte green, GLubyte blue) {
			if (data != NULL) {
				if (format == GL_RGBA) {
					int bc = width * height * depth * 4;
					for (int i = 0; i < bc; i += 4)
						if (data[i] == red && data[i+1] == green && data[i+2] == blue)
							data[i+3] = newAlpha;
					return true;
				} else if (format == GL_BGRA) {
					int bc = width * height * depth * 4;
					for (int i = 0; i < bc; i += 4)
						if (data[i] == blue && data[i+1] == green && data[i+2] == red)
							data[i+3] = newAlpha;
					return true;
				}
			}
			return false;
		}

		bool Pixmap::setColorWhereAlpha(GLubyte newRed, GLubyte newGreen, GLubyte newBlue, GLubyte alpha) {
			if (data != NULL) {
				if (format == GL_RGBA) {
					int bc = width * height * depth * 4;
					for (int i = 0; i < bc; i += 4)
						if (data[i+3] == alpha) {
							data[i  ] = newRed;
							data[i+1] = newGreen;
							data[i+2] = newBlue;
						}
					return true;
				} else if (format == GL_BGRA) {
					int bc = width * height * depth * 4;
					for (int i = 0; i < bc; i += 4)
						if (data[i+3] == alpha) {
							data[i  ] = newBlue;
							data[i+1] = newGreen;
							data[i+2] = newRed;
						}
					return true;
				}
			}
			return false;
		}

		// Helper functions
		#ifdef __ZEN_LOADER_PIX__
		#define __PIX_MAGIC_NUMBER__ 0x5A83FC02
		// --- EPIX format -----------------
		// GLuint magicNumber;
		// GLuint imageWidth;
		// GLuint imageHeight;
		// GLuint imageDepth;
		// GLenum imageFormat;
		// GLubyte imageData[imageWidth*imageHeight*imageDepth*bytesPerPixel];
		// ---------------------------------
		bool Pixmap::loadPIX(const std::string &fileName) {
			GLuint tmp4;
			bool res = true;

			FILE* file = NULL;
			errno_t error = fopen_s(&file, fileName.c_str(), "rb");
			if(error != 0 || file == NULL) {
				data = NULL;
				width = height = depth = 0;
				format = GL_RGB;
				return false;
			}

			res = res && fread((char*)&tmp4, 4, 1, file);
			if (tmp4 != __PIX_MAGIC_NUMBER__) {
				fclose(file);
				delete[] data;
				data = NULL;
				width = height = depth = 0;
				format = GL_RGB;
				return false;
			}

			res = res && fread((char*)&width, 4, 1, file);
			res = res && fread((char*)&height, 4, 1, file);
			res = res && fread((char*)&depth, 4, 1, file);
			res = res && fread((char*)&format, 4, 1, file);
			if (!res) {
				fclose(file);
				delete[] data;
				data = NULL;
				width = height = depth = 0;
				format = GL_RGB;
				return false;
			}

			GLubyte bpp = formatBitCount() / 8;
			GLuint bc = width * height * depth * bpp;

			delete[] data;
			data = new GLubyte[bc];
			if (fread((char*)data, bc, 1, file) != 1) {
				fclose(file);
				delete[] data;
				data = NULL;
				width = height = depth = 0;
				format = GL_RGB;
				return false;
			}

			return true;
		}
		bool Pixmap::savePIX(const std::string &fileName) const {
			if (data == NULL)
				return false;

			GLubyte bpp = formatBitCount() / 8;
			GLuint bc = width * height * depth * bpp;

			FILE* file = NULL;
			errno_t error = fopen_s(&file, fileName.c_str(), "wb");
			if(error != 0 || file == NULL)
				return false;

			GLuint tmp4 = __PIX_MAGIC_NUMBER__;

			fwrite((char*)&tmp4, 4, 1, file);
			fwrite((char*)&width, 4, 1, file);
			fwrite((char*)&height, 4, 1, file);
			fwrite((char*)&depth, 4, 1, file);
			fwrite((char*)&format, 4, 1, file);

			bool res = fwrite((char*)data, bc, 1, file) == 1;
			fclose(file);
			return res;
		}
		#endif

		#ifdef __ZEN_LOADER_BMP__
		bool Pixmap::loadBMP(const std::string &fileName) {
			GLushort    tmp2;
			GLuint      tmp4;
			char tmp64[64];
			bool res = true;

			FILE* file = NULL;
			errno_t error = fopen_s(&file, fileName.c_str(), "rb");
			if(error != 0 || file == NULL) {
				delete[] data;
				data = NULL;
				width = height = depth = 0;
				format = GL_RGB;
				return false;
			}

			// File header
			res = res && fread(tmp64, 14, 1, file);
			// Bmp header
			res = res && fread((char*)&tmp4, 4, 1, file);
			res = res && fread((char*)&tmp4, 4, 1, file);
			width = tmp4;
			res = res && fread((char*)&tmp4, 4, 1, file);
			height = tmp4;
			res = res && fread((char*)&tmp2, 2, 1, file);
			depth = tmp2;
			GLushort bits;
			res = res && fread((char*)&bits, 2, 1, file);
			res = res && fread(tmp64, 24, 1, file);

			GLubyte bpp = bits / 8;
			GLuint bc = width * height * depth * bpp;

			if (res==false || bpp<1 || bpp>4) {
    			delete[] data;
				data = NULL;
				width = height = depth = 0;
				format = GL_RGB;
				fclose(file);
				return false;
			}
			else if (bpp==4)
				format = GL_BGRA;
			else if (bpp==3)
				format = GL_BGR;
			else if (bpp==2)
				format = GL_LUMINANCE_ALPHA;
			else if (bpp==1)
				format = GL_ALPHA;

			data = new GLubyte[bc];
			if (data != NULL) {
    			if (fread((char*)data, bc, 1, file) == 1) {
    				fclose(file);
    				return true;
    			}
			}

			delete[] data;
			data = NULL;
			width = height = depth = 0;
			format = GL_RGB;
			fclose(file);
			return false;
		}
		bool Pixmap::saveBMP(const std::string &fileName) const {
			if (data == NULL)
				return false;

			GLubyte bpp = formatBitCount() / 8;
			GLuint bc = width * height * depth * bpp;

			GLushort    fType = 19778;
			GLuint      fSize = 54 + bc;
			GLushort    fReserved1 = 0;
			GLushort    fReserved2 = 0;
			GLuint      fOffsetBytes = 54;

			GLuint      hSize = 40;
			GLuint      hWidth = width;
			GLuint      hHeight = height;
			GLushort    hPlanes = depth;
			GLushort    hBitCount = bpp * 8;
			GLuint      hCompression = 0;
			GLuint      hSizeImage = bc;
			GLint       hXpelspermeter = 3780;
			GLint       hYpelspermeter = 3780;
			GLuint      hColorsUsed = 0;
			GLuint      hColorsImportant = 0;

			FILE* file = NULL;
			errno_t error = fopen_s(&file, fileName.c_str(), "wb");
			if (error != 0 || file == NULL)
				return false;

			fwrite((char*)&fType, 2, 1, file);
			fwrite((char*)&fSize, 4, 1, file);
			fwrite((char*)&fReserved1, 2, 1, file);
			fwrite((char*)&fReserved2, 2, 1, file);
			fwrite((char*)&fOffsetBytes, 4, 1, file);

			fwrite((char*)&hSize, 4, 1, file);
			fwrite((char*)&hWidth, 4, 1, file);
			fwrite((char*)&hHeight, 4, 1, file);
			fwrite((char*)&hPlanes, 2, 1, file);
			fwrite((char*)&hBitCount, 2, 1, file);
			fwrite((char*)&hCompression, 4, 1, file);
			fwrite((char*)&hSizeImage, 4, 1, file);
			fwrite((char*)&hXpelspermeter, 4, 1, file);
			fwrite((char*)&hYpelspermeter, 4, 1, file);
			fwrite((char*)&hColorsUsed, 4, 1, file);
			fwrite((char*)&hColorsImportant, 4, 1, file);

			bool res = fwrite((char*)data, bc, 1, file) == 1;
			fclose(file);
			return res;
		}
		#endif

		#ifdef __ZEN_LOADER_PNG__
		bool Pixmap::loadPNG(const std::string fileName) {
			return false;
		}
		bool Pixmap::savePNG(const std::string fileName) const {
			return false;
		}
		#endif

		#ifdef __ZEN_LOADER_TGA__
		bool Pixmap::loadTGA(const std::string fileName) {
			return false;
		}
		bool Pixmap::saveTGA(const std::string fileName) const {
			return false;
		}
		#endif


	} // namespace video

} // namespace zen
